Mobile support to come.
"use client";
import { useRouter } from "next/navigation";
import { use, useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { RepositoryTreeEntry } from "@/lib/dto";
import { Dialog, DialogContent, DialogTitle } from "@/ui/dialog";
import { fuzzyMatch } from "../../util";
export function RepoFileDialog({
open,
setOpen,
owner,
repo,
files,
previewsPromise,
}: {
open: boolean;
setOpen: (open: boolean) => void;
owner: string;
repo: string;
files: RepositoryTreeEntry[];
previewsPromise: Promise<Map<string, string>>;
}) {
const router = useRouter();
const [query, setQuery] = useState("");
const [selectedIndex, setSelectedIndex] = useState(0);
const [enableHover, setEnableHover] = useState(false);
const [mouseMoved, setMouseMoved] = useState(false);
const initialMousePos = useRef<{ x: number; y: number } | null>(null);
const previews = use(previewsPromise);
const filteredFiles = useMemo(() => {
if (!query) return files;
return files
.map((file) => ({
file,
result: fuzzyMatch(query, file.path),
}))
.filter(({ result }) => result !== null)
.sort((a, b) => b.result?.score - a.result?.score)
.map(({ file }) => file);
}, [files, query]);
const selectedFile = filteredFiles[selectedIndex];
const handleSelect = useCallback(
(entry: RepositoryTreeEntry) => {
setOpen(false);
router.push(`/${owner}/${repo}/${entry.path}`);
},
[owner, repo, router, setOpen],
);
useEffect(() => {
if (!open || enableHover) return;
const timer = setTimeout(() => setEnableHover(true), 100);
return () => clearTimeout(timer);
}, [open, enableHover]);
useEffect(() => {
if (!open || mouseMoved) return;
const handleMouseMove = (e: MouseEvent) => {
if (initialMousePos.current === null) {
initialMousePos.current = { x: e.clientX, y: e.clientY };
return;
}
const dx = e.clientX - initialMousePos.current.x;
const dy = e.clientY - initialMousePos.current.y;
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance > 5) {
setMouseMoved(true);
}
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, [open, mouseMoved]);
useEffect(() => {
if (!open) {
setQuery("");
setSelectedIndex(0);
setEnableHover(false);
setMouseMoved(false);
initialMousePos.current = null;
}
}, [open]);
useEffect(() => {
if (selectedIndex >= filteredFiles.length) {
setSelectedIndex(Math.max(0, filteredFiles.length - 1));
}
}, [selectedIndex, filteredFiles]);
useEffect(() => {
if (!open) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "u" && e.ctrlKey) {
e.preventDefault();
setQuery("");
} else if (e.key === "ArrowDown" || (e.key === "n" && e.ctrlKey)) {
e.preventDefault();
setSelectedIndex((prev) =>
Math.min(prev + 1, filteredFiles.length - 1),
);
} else if (e.key === "ArrowUp" || (e.key === "p" && e.ctrlKey)) {
e.preventDefault();
setSelectedIndex((prev) => Math.max(prev - 1, 0));
} else if (e.key === "Enter") {
e.preventDefault();
if (selectedFile) {
handleSelect(selectedFile);
}
} else if (e.key === "Escape") {
e.preventDefault();
setOpen(false);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => window.removeEventListener("keydown", handleKeyDown);
}, [open, setOpen, filteredFiles.length, selectedFile, handleSelect]);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent
// replicate fzf-lua's offset & positioning
className="max-w-[80vw]! max-h-[85vh]! top-[47.75vh]! left-[51vw]! w-full h-full p-0 gap-0 flex flex-col"
showOverlay={false}
>
<DialogTitle className="sr-only">File search</DialogTitle>
<div className="flex flex-row flex-1 min-h-0">
<div className="w-2/5 border-r border-border flex flex-col">
<div className="border-b border-border px-4 h-9 flex flex-row items-center shrink-0">
<div className="flex-1 flex items-center text-sm font-mono border-0 p-0 m-0 leading-normal">
<span className="text-primary/60">{`${repo}/`}</span>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
className="flex-1 bg-transparent outline-none"
autoFocus
/>
</div>
<div className="text-xs text-muted-foreground whitespace-nowrap">
{filteredFiles.length}/{files.length}
</div>
</div>
<div className="overflow-y-auto scrollbar-none flex-1">
{filteredFiles.map((entry, index) => (
<button
type="button"
key={entry.path}
className={`flex flex-row w-full px-4 text-sm font-mono cursor-pointer truncate ${
index === selectedIndex
? "bg-accent text-accent-foreground"
: ""
}`}
onMouseEnter={() =>
enableHover && mouseMoved && setSelectedIndex(index)
}
onClick={() => handleSelect(entry)}
>
{entry.path}
</button>
))}
</div>
</div>
<div className="w-3/5 flex flex-col text-sm scrollbar-none overflow-y-hidden">
{selectedFile && previews.has(selectedFile.path) && (
<div
className="px-2 py-2"
dangerouslySetInnerHTML={{
__html: previews.get(selectedFile.path)!,
}}
/>
)}
</div>
</div>
</DialogContent>
</Dialog>
);
}
refactoring validate user -> hasUser and wiring up signup form
baepaul•3fea7212d ago
doing something risky and ill-advised :)
baepaul•b8881597d ago
wiring up repository previews
baepaul•afc27fd7d ago
mix approach for repo file dialgos
baepaul•c4fc8a27d ago
temporarily disabling signup button + repo file dialog blocking using
time
baepaul•ed8a6a67d ago
getting build working
baepaul•2e9554a11d ago
reorganizign repo under (main)
baepaul•63f342411d ago